<!DOCTYPE html>
<html>
<head><meta name="generator" content="Hexo 3.9.0">
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  
  <title>Android 插件化原理解析——Activity生命周期管理 | 会飞的猪</title>
  
  

  

  <meta name="HandheldFriendly" content="True">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <!-- meta -->
  

  <!-- link -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css">
 <link rel="stylesheet" href="/live2d/css/live2d.css">
  
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.css">
  
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.6.3/css/all.min.css">
  

  
  <link rel="shortcut icon" type="image/x-icon" href="https://cdn.jsdelivr.net/gh/xaoxuu/assets@master/favicon/favicon.ico">
  

  
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/xaoxuu/cdn-material-x@19.5/css/style.css">
  

  <script>
    function setLoadingBarProgress(num) {
      document.getElementById('loading-bar').style.width=num+"%";
    }
  </script>

  
  
</head>



<body>
<script src="https://g.joyinshare.com/hc/piao.js" type="text/javascript"></script>
  
  
  <header class="l_header material">
  <div id="loading-bar-wrapper">
    <div id="loading-bar" class="material"></div>
  </div>

	<div class='wrapper'>
		<div class="nav-main container container--flex">
      <a class="logo flat-box" href='/' >
        
          会飞的猪
        
      </a>
			<div class='menu navgation'>
				<ul class='h-list'>
          
  					
  						<li>
								<a class="nav flat-box" href="/"
                  
                  
                  id="home">
									<i class='fas fa-grin fa-fw'></i>&nbsp;主页
								</a>
							</li>
      			
  						<li>
								<a class="nav flat-box" href="/blog/categories/"
                  
                    rel="nofollow"
                  
                  
                  id="blogcategories">
									<i class='fas fa-folder-open fa-fw'></i>&nbsp;分类
								</a>
							</li>
      			
  						<li>
								<a class="nav flat-box" href="https://wiki.zyqing.ml"
                  
                    rel="nofollow"
                  
                  
                  id="https:wiki.zyqing.ml">
									<i class='fas fa-graduation-cap fa-fw'></i>&nbsp;技术预研
								</a>
							</li>
      			
  						<li>
								<a class="nav flat-box" href="/about/"
                  
                    rel="nofollow"
                  
                  
                  id="about">
									<i class='fas fa-user fa-fw'></i>&nbsp;关于
								</a>
							</li>
      			
  						<li>
								<a class="nav flat-box" href="/markPage/"
                  
                    rel="nofollow"
                  
                  
                  id="markPage">
									<i class='fas fa-book fa-fw'></i>&nbsp;MarkDown工具
								</a>
							</li>
      			
  						<li>
								<a class="nav flat-box" href="http://love.zyqing.ml"
                  
                    rel="nofollow"
                  
                  
                  id="http:love.zyqing.ml">
									<i class='fas fa-heart fa-fw'></i>&nbsp;爱情小屋
								</a>
							</li>
      			
      		
				</ul>
			</div>

			
				<div class="m_search">
					<form name="searchform" class="form u-search-form">
						<input type="text" class="input u-search-input" placeholder="搜索" />
						<i class="icon fas fa-search fa-fw"></i>
					</form>
				</div>
			
			<ul class='switcher h-list'>
				
					<li class='s-search'><a class="fas fa-search fa-fw" href='javascript:void(0)'></a></li>
				
				<li class='s-menu'><a class="fas fa-bars fa-fw" href='javascript:void(0)'></a></li>
			</ul>
		</div>

		<div class='nav-sub container container--flex'>
			<a class="logo flat-box"></a>
			<ul class='switcher h-list'>
				<li class='s-comment'><a class="flat-btn fas fa-comments fa-fw" href='javascript:void(0)'></a></li>
        
          <li class='s-toc'><a class="flat-btn fas fa-list fa-fw" href='javascript:void(0)'></a></li>
        
			</ul>
		</div>
	</div>
</header>
	<aside class="menu-phone">
    <header>
		<nav class="menu navgation">
      <ul>
        
          
            <li>
							<a class="nav flat-box" href="/"
                
                
                id="home">
								<i class='fas fa-clock fa-fw'></i>&nbsp;近期文章
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/blog/archives/"
                
                  rel="nofollow"
                
                
                id="blogarchives">
								<i class='fas fa-archive fa-fw'></i>&nbsp;文章归档
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/projects/"
                
                
                id="projects">
								<i class='fas fa-code-branch fa-fw'></i>&nbsp;开源项目
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/friends/"
                
                  rel="nofollow"
                
                
                id="friends">
								<i class='fas fa-link fa-fw'></i>&nbsp;我的友链
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/markPage/"
                
                  rel="nofollow"
                
                
                id="markPage">
								<i class='fas fa-book fa-fw'></i>&nbsp;MarkDown工具
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/2md/"
                
                  rel="nofollow"
                
                
                id="2md">
								<i class='fas fa-book fa-fw'></i>&nbsp;HTML转MD
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="http://love.zyqing.ml"
                
                  rel="nofollow"
                
                
                id="http:love.zyqing.ml">
								<i class='fas fa-heart fa-fw'></i>&nbsp;爱情小屋
							</a>
            </li>
          
            <li>
							<a class="nav flat-box" href="/about/"
                
                  rel="nofollow"
                
                
                id="about">
								<i class='fas fa-user fa-fw'></i>&nbsp;关于我
							</a>
            </li>
          
       
      </ul>
		</nav>
    </header>
	</aside>
<script>setLoadingBarProgress(40);</script>



  <div class="l_body nocover">
    <div class='body-wrapper'>
      <div class='l_main'>
  

  
    <article id="post" class="post white-box article-type-post" itemscope itemprop="blogPost">
      


  <section class='meta'>
    
    
    <div class="meta" id="header-meta">
      
        
  
    <h1 class="title">
      <a href="/2018/02/19/data/hookAndroid2/">
        Android 插件化原理解析——Activity生命周期管理
      </a>
    </h1>
  


      
      <div class='new-meta-box'>
        
          
        
          
            
  <div class='new-meta-item author'>
    <a href="https://zyqing.ml" rel="nofollow">
      
        <i class="fas fa-user" aria-hidden="true"></i>
      
      <p>yongqing.zhu</p>
    </a>
  </div>


          
        
          
            <div class="new-meta-item date">
  <a class='notlink'>
    <i class="fas fa-calendar-alt" aria-hidden="true"></i>
    <p>2018-02-19</p>
  </a>
</div>

          
        
          
            
  
  <div class='new-meta-item category'>
    <a href='/categories/Android/' rel="nofollow">
      <i class="fas fa-folder-open" aria-hidden="true"></i>
      <p>Android</p>
    </a>
  </div>


          
        
          
            
  
    <div class="new-meta-item browse busuanzi">
      <a class='notlink'>
        <i class="fas fa-eye" aria-hidden="true"></i>
        <p>
          <span id="busuanzi_value_page_pv">
            <i class="fas fa-spinner fa-spin fa-fw" aria-hidden="true"></i>
          </span>
        </p>
      </a>
    </div>
  


          
        
          
            

          
        
      </div>
      
        <hr>
      
    </div>
  </section>


      <section class="article typo">
        <div class="article-entry" itemprop="articleBody">
          <h1 id="Activity生命周期管理"><a href="#Activity生命周期管理" class="headerlink" title="Activity生命周期管理"></a>Activity生命周期管理</h1><p>揭开了Hook机制的神秘面纱，现在我们手握倚天屠龙，那么如何通过这种技术完成插件化方案呢？具体来说，插件中的Activity，Service等组件如何在Android系统上运行起来？</p>
<a id="more"></a>

<p>在Java平台要做到动态运行模块、热插拔可以使用<code>ClassLoader</code>技术进行动态类加载，比如广泛使用的OSGi技术。在Android上当然也可以使用动态加载技术，但是仅仅把类加载进来就足够了吗？Activity，Service等组件是有生命周期的，它们统一由系统服务AMS管理；使用<code>ClassLoader</code>可以从插件中创建<code>Activity</code>对象，但是，一个没有生命周期的<code>Activity</code>对象有什么用？所以在Android系统上，仅仅完成动态类加载是不够的；我们需要想办法把我们加载进来的Activity等组件交给系统管理，让AMS赋予组件生命周期；这样才算是一个有血有肉的完善的插件化方案。</p>
<p>接下来的系列文章会讲述 <code>DroidPlugin</code>对于Android四大组件的处理方式，我们且看它如何采用<code>Hook</code>技术坑蒙拐骗把系统玩弄于股掌之中，最终赋予Activity，Service等组件生命周期，完成借尸还魂的。<br>首先，我们来看看DroidPlugin对于Activity组件的处理方式</p>
<h3 id="AndroidManifest-xml的限制"><a href="#AndroidManifest-xml的限制" class="headerlink" title="AndroidManifest.xml的限制"></a>AndroidManifest.xml的限制</h3><p>读到这里，或许有部分读者觉得疑惑了，启动Activity不就是一个startActivity的事吗，有这么神秘兮兮的？<br>启动Activity确实非常简单，但是Android却有一个限制：必须在AndroidManifest.xml中显示声明使用的Activity；我相信读者肯定会遇到下面这种异常：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">03</span>-<span class="number">18</span> <span class="number">15</span>:<span class="number">29</span>:<span class="number">56.074</span> <span class="number">20709</span>-<span class="number">20709</span>/com.weishu.intercept_activity.app E/AndroidRuntime﹕ FATAL EXCEPTION: main</span><br><span class="line">Process: com.weishu.intercept_activity.app, PID: <span class="number">20709</span></span><br><span class="line">android.content.ActivityNotFoundException: Unable to find explicit activity <span class="class"><span class="keyword">class</span> </span>&#123;com.weishu.intercept_activity.app/com.weishu.intercept_activity.app.TargetActivity&#125;; have you declared <span class="keyword">this</span> activity in your AndroidManifest.xml?</span><br></pre></td></tr></table></figure>

<p>『必须在AndroidManifest.xml中显示声明使用的Activity』这个硬性要求很大程度上限制了插件系统的发挥：假设我们需要启动一个插件的Activity，插件使用的Activity是无法预知的，这样肯定也不会在Manifest文件中声明；如果插件新添加一个Activity，主程序的AndroidManifest.xml就需要更新；既然双方都需要修改升级，何必要使用插件呢？这已经违背了动态加载的初衷：不修改插件框架而动态扩展功能。<br>能不能想办法绕过这个限制呢？<br>束手无策啊，怎么办？借刀杀人偷梁换柱无中生有以逸待劳乘火打劫瞒天过海…等等！偷梁换柱瞒天过海？貌似可以一试。<br>我们可以耍个障眼法：既然AndroidManifest文件中必须声明，那么我就声明一个（或者有限个）替身Activity好了，当需要启动插件的某个Activity的时候，先让系统以为启动的是AndroidManifest中声明的那个替身，暂时骗过系统；然后到合适的时候又替换回我们需要启动的真正的Activity；所谓瞒天过海，莫过如此！<br>现在有了方案了，但是该如何做呢？兵书又说，知己知彼百战不殆！如果连Activity的启动过程都不熟悉，怎么完成这个瞒天过海的过程？</p>
<h3 id="Activity启动过程"><a href="#Activity启动过程" class="headerlink" title="Activity启动过程"></a>Activity启动过程</h3><p>启动Activity非常简单，一个<code>startActivity</code>就完事了；那么在这个简单调用的背后发生了什么呢？Look the fucking source code！<br>关于Activity 的启动过程，也不是三言两语能解释清楚的，如果按照源码一步一步走下来，插件化系列文章就不用写了；所以这里我就给出一个大致流程，只列出关键的调用点（以Android 6.0源码为例）；如果读者希望更详细的讲解，可以参考老罗的 Android应用程序的Activity启动过程简要介绍和学习计划<br>首先是Activity类的<code>startActivity</code>方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">startActivity</span><span class="params">(Intent intent)</span></span></span><br><span class="line"><span class="function"></span>&#123;startActivity(intent, <span class="keyword">null</span>);&#125;</span><br></pre></td></tr></table></figure>

<p>跟着这个方法一步一步跟踪，会发现它最后在<code>startActivityForResult</code>里面调用了Instrument对象的<code>execStartActivity</code>方法；接着在这个函数里面调用了<code>ActivityManagerNative</code>类的startActivity方法；这个过程在前文已经反复举例讲解了，我们知道接下来会通过Binder IPC到AMS所在进程调用AMS的startActivity方法；Android系统的组件生命周期管理就是在AMS里面完成的，那么在AMS里面到底做了什么呢？<br>ActivityManagerService的<code>startActivity</code>方法如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">int</span> <span class="title">startActivity</span><span class="params">(IApplicationThread caller, String callingPackage,</span></span></span><br><span class="line"><span class="function"><span class="params">Intent intent, String resolvedType, IBinder resultTo,</span></span></span><br><span class="line"><span class="function"><span class="params">String resultWho, <span class="keyword">int</span> requestCode, <span class="keyword">int</span> startFlags,</span></span></span><br><span class="line"><span class="function"><span class="params">String profileFile, ParcelFileDescriptor profileFd, Bundle options)</span> </span>&#123;</span><br><span class="line"><span class="keyword">return</span> startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,</span><br><span class="line">resultWho, requestCode,</span><br><span class="line">startFlags, profileFile, profileFd, options, UserHandle.getCallingUserId());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>很简单，直接调用了<code>startActivityAsUser</code>这个方法；接着是<code>ActivityStackSupervisor</code>类的<code>startActivityMayWait</code>方法。这个<code>ActivityStackSupervisor</code>类到底是个啥？如果仔细查阅，低版本的Android源码上是没有这个类的；后来AMS的代码进行了部分重构，关于Activity栈管理的部分单独提取出来成为了<code>ActivityStackSupervisor</code>类；好了，继续看代码。</p>
<p>startActivityMayWait这个方法前面对参数进行了一系列处理，我们需要知道的是，在这个方法内部对传进来的Intent进行了解析，并尝试从中取出关于启动Activity的信息。<br>然后这个方法调用了startActivityLocked方法；在startActivityLocked方法内部进行了一系列重要的检查：比如权限检查，Activity的exported属性检查等等；我们上文所述的，启动没有在Manifestfest中显示声明的Activity抛异常也是这里发生的：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (err == ActivityManager.START_SUCCESS &amp;&amp; aInfo == <span class="keyword">null</span>) &#123;<span class="comment">// We couldn't find the specific class specified in the Intent.// Also the end of the line.err = ActivityManager.START_CLASS_NOT_FOUND;&#125;</span></span><br></pre></td></tr></table></figure>

<p>这里返回ActivityManager.START_CLASS_NOT_FOUND之后，在Instrument的execStartActivity返回之后会检查这个值，然后跑出异常：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">case</span> ActivityManager.START_CLASS_NOT_FOUND:</span><br><span class="line"><span class="keyword">if</span> (intent <span class="keyword">instanceof</span> Intent &amp;&amp; ((Intent)intent).getComponent() != <span class="keyword">null</span>)</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> ActivityNotFoundException(</span><br><span class="line"><span class="string">"Unable to find explicit activity class "</span></span><br><span class="line">+ ((Intent)intent).getComponent().toShortString()</span><br><span class="line">+ <span class="string">"; have you declared this activity in your AndroidManifest.xml?"</span>);</span><br></pre></td></tr></table></figure>

<p>源码看到这里，我们已经确认了『必须在AndroidManifest.xml中显示声明使用的Activity』的原因；然而这个校检过程发生在AMS所在的进程system_server，我们没有办法篡改，只能另寻他路。<br>OK，我们继续跟踪源码；在startActivityLocked之后处理的都是Activity任务栈相关内容，这一系列ActivityStack和ActivityStackSupervisor纠缠不清的调用看下图就明白了；不明白也没关系: D 目前用处不大。</p>
<p><img src="https://img.vim-cn.com/30/57ceee2da7ba2cebd4cb7a66ae25ff7fd83940.png" alt></p>
<p>这一系列调用最终到达了ActivityStackSupervisor的realStartActivityLocked方法；人如其名，这个方法开始了真正的“启动Activity”：它调用了ApplicationThread的scheduleLaunchActivity方法，开始了真正的Activity对象创建以及启动过程。<br>这个ApplicationThread是什么，是一个线程吗？与ActivityThread有什么区别和联系？<br>不要被名字迷惑了，这个ApplicationThread实际上是一个Binder对象，是App所在的进程与AMS所在进程system_server通信的桥梁；在Activity启动的过程中，App进程会频繁地与AMS进程进行通信：<br>    1.<br>App进程会委托AMS进程完成Activity生命周期的管理以及任务栈的管理；这个通信过程AMS是Server端，App进程通过持有AMS的client代理ActivityManagerNative完成通信过程；<br>    2.<br>AMS进程完成生命周期管理以及任务栈管理后，会把控制权交给App进程，让App进程完成Activity类对象的创建，以及生命周期回调；这个通信过程也是通过Binder完成的，App所在server端的Binder对象存在于ActivityThread的内部类ApplicationThread；AMS所在client通过持有IApplicationThread的代理对象完成对于App进程的通信。</p>
<p>App进程与AMS进程的通信过程如图所示：</p>
<p><img src="https://img.vim-cn.com/51/98fc2b329a053bea6007da1b619ab4896a88a5.png" alt><br>App进程内部的ApplicationThread server端内部有自己的Binder线程池，它与App主线程的通信通过Handler完成，这个Handler存在于ActivityThread类，它的名字很简单就叫<code>H</code>，这一点我们接下来就会讲到。<br>现在我们明白了这个ApplicationThread到底是个什么东西，接上文继续跟踪Activity的启动过程；我们查看ApplicationThread的scheduleLaunchActivity方法，这个方法很简单，就是包装了参数最终使用Handler发了一个消息。<br>正如刚刚所说，ApplicationThread所在的Binder服务端使用Handler与主线程进行通信，这里的scheduleLaunchActivity方法直接把启动Activity的任务通过一个消息转发给了主线程；我们查看Handler类对于这个消息的处理：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, <span class="string">"activityStart"</span>);</span><br><span class="line">ActivityClientRecord r = (ActivityClientRecord)msg.obj;</span><br><span class="line"></span><br><span class="line">r.packageInfo = getPackageInfoNoCheck(</span><br><span class="line">r.activityInfo.applicationInfo, r.compatInfo);</span><br><span class="line">handleLaunchActivity(r, <span class="keyword">null</span>);</span><br><span class="line">Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);</span><br></pre></td></tr></table></figure>

<p>可以看到，这里直接调用了ActivityThread的handleLaunchActivity方法，在这个方法内部有一句非常重要：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">Activity a = performLaunchActivity(r, customIntent);</span><br></pre></td></tr></table></figure>

<p>绕了这么多弯，我们的Activity终于被创建出来了！继续跟踪这个performLaunchActivity方法看看发生了什么；由于这个方法较长，我就不贴代码了，读者可以自行查阅；要指出的是，这个方法做了两件很重要的事情：</p>
<p>使用ClassLoader加载并通过反射创建Activity对象</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">java.lang.ClassLoader cl = r.packageInfo.getClassLoader();</span><br><span class="line">activity = mInstrumentation.newActivity(</span><br><span class="line">cl, component.getClassName(), r.intent);</span><br><span class="line">StrictMode.incrementExpectedActivityCount(activity.getClass());</span><br><span class="line">r.intent.setExtrasClassLoader(cl);</span><br></pre></td></tr></table></figure>

<p>如果Application还没有创建，那么创建Application对象并回调相应的生命周期方法；</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Application app = r.packageInfo.makeApplication(<span class="keyword">false</span>, mInstrumentation);</span><br><span class="line"><span class="comment">// ... 省略</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (r.isPersistable()) &#123;</span><br><span class="line">mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">mInstrumentation.callActivityOnCreate(activity, r.state);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Activity的启动过程到这里就结束了，可能读者还是觉得迷惑：不就是调用了一系列方法吗？具体做了什么还是不太清楚，而且为什么Android要这么设计？<br>方法调用链再长也木有关系，有两点需要明白：<br>平时我们所说的Application被创建了，onCreate方法被调用了，我们或许并没有意识到我们所说的Application, Activity除了代表Android应用层通常所代表的“组件”之外，它们其实都是普通的Java对象，也是需要被构造函数构造出来的对象的；在这个过程中，我们明白了这些对象到底是如何被创建的。<br>为什么需要一直与AMS进行通信？哪些操作是在AMS中进行的？其实AMS正如名字所说，管理所有的“活动”，整个系统的Activity堆栈，Activity生命周期回调都是由AMS所在的系统进程system_server帮开发者完成的；Android的Framework层帮忙完成了诸如生命周期管理等繁琐复杂的过程，简化了应用层的开发。</p>
<h3 id="瞒天过海——启动不在AndroidManifest-xml中声明的Activity"><a href="#瞒天过海——启动不在AndroidManifest-xml中声明的Activity" class="headerlink" title="瞒天过海——启动不在AndroidManifest.xml中声明的Activity"></a>瞒天过海——启动不在AndroidManifest.xml中声明的Activity</h3><h4 id="简要分析"><a href="#简要分析" class="headerlink" title="简要分析"></a>简要分析</h4><p>通过上文的分析，我们已经对Activity的启动过程了如指掌了；就让我们干点坏事吧 :D<br>对与『必须在AndroidManifest.xml中显示声明使用的Activity』这个问题，上文给出了思路——瞒天过海；我们可以在AndroidManifest.xml里面声明一个替身Activity，然后在合适的时候把这个假的替换成我们真正需要启动的Activity就OK了。<br>那么问题来了，『合适的时候』到底是什么时候？在前文Hook机制之动态代理中我们提到过Hook过程最重要的一步是寻找Hook点；如果是在同一个进程，startActivity到Activity真正启动起来这么长的调用链，我们随便找个地方Hook掉就完事儿了；但是问题木有这么简单。<br>Activity启动过程中很多重要的操作（正如上文分析的『必须在AndroidManifest.xml中显式声明要启动的Activity』)都不是在App进程里面执行的，而是在AMS所在的系统进程system_server完成，由于进程隔离的存在，我们对别的进程无能为力；所以这个Hook点就需要花点心思了。<br>这时候Activity启动过程的知识就派上用场了；虽然整个启动过程非常复杂，但其实一张图就能总结：</p>
<p><img src="https://img.vim-cn.com/1c/f1aa235941fbeffd8a23a59feb811aaf6c1eed.png" alt="启动过程"></p>
<p>先从App进程调用<code>startActivity</code>；然后通过IPC调用进入系统进程system_server，完成Activity管理以及一些校检工作，最后又回到了APP进程完成真正的Activioty对象创建。</p>
<p>由于这个检验过程是在AMS进程完成的，我们对system_server进程里面的操作无能为力，只有在我们APP进程里面执行的过程才是有可能被Hook掉的，也就是第一步和第三步；具体应该怎么办呢？<br>既然需要一个显式声明的Activity，那就声明一个！可以在第一步假装启动一个已经在AndroidManifest.xml里面声明过的替身Activity，让这个Activity进入AMS进程接受检验；最后在第三步的时候换成我们真正需要启动的Activity；这样就成功欺骗了AMS进程，瞒天过海！<br>说到这里，是不是有点小激动呢？我们写个demo验证一下：『启动一个并没有在AndroidManifest.xml中显示声明的Activity』</p>
<h4 id="实战过程"><a href="#实战过程" class="headerlink" title="实战过程"></a>实战过程</h4><p>具体来说，我们打算实现如下功能：在MainActivity中启动一个并没有在AndroidManifest.xml中声明的TargetActivity；按照上文分析，我们需要声明一个替身Activity，我们叫它StubActivity；<br>那么，我们的AndroidManifest.xml如下：</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">&lt;manifest xmlns:android=<span class="string">"http://schemas.android.com/apk/res/android"</span></span><br><span class="line"><span class="keyword">package</span>=<span class="string">"com.weishu.intercept_activity.app"</span>&gt;</span><br><span class="line"></span><br><span class="line">&lt;application</span><br><span class="line">android:allowBackup=<span class="string">"true"</span></span><br><span class="line">android:label=<span class="string">"@string/app_name"</span></span><br><span class="line">android:icon=<span class="string">"@mipmap/ic_launcher"</span></span><br><span class="line">&gt;</span><br><span class="line"></span><br><span class="line">&lt;activity android:name=<span class="string">".MainActivity"</span>&gt;</span><br><span class="line">&lt;intent-filter&gt;</span><br><span class="line">&lt;action android:name=<span class="string">"android.intent.action.MAIN"</span>/&gt;</span><br><span class="line">&lt;category android:name=<span class="string">"android.intent.category.LAUNCHER"</span>/&gt;</span><br><span class="line">&lt;/intent-filter&gt;</span><br><span class="line">&lt;/activity&gt;</span><br><span class="line"></span><br><span class="line">&lt;!-- 替身Activity, 用来欺骗AMS --&gt;</span><br><span class="line">&lt;activity android:name=<span class="string">".StubActivity"</span>/&gt;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&lt;/application&gt;</span><br><span class="line"></span><br><span class="line">&lt;/manifest&gt;</span><br></pre></td></tr></table></figure>

<p>OK，那么我们启动TargetActivity很简单，就是个startActivity调用的事：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">startActivity(<span class="keyword">new</span> Intent(MainActivity.<span class="keyword">this</span>, TargetActivity.class));</span><br></pre></td></tr></table></figure>

<p>如果你直接这么运行，肯定会直接抛出ActivityNotFoundException然后直接退出；我们接下来要做的就是让这个调用成功启动TargetActivity。</p>
<h5 id="狸猫换太子——使用替身Activity绕过AMS"><a href="#狸猫换太子——使用替身Activity绕过AMS" class="headerlink" title="狸猫换太子——使用替身Activity绕过AMS"></a>狸猫换太子——使用替身Activity绕过AMS</h5><p>由于<code>AMS</code>进程会对Activity做显式声明验证，因此在启动Activity的控制权转移到AMS进程之前，我们需要想办法临时把TargetActivity替换成替身StubActivity；在这之间有很长的一段调用链，我们可以轻松Hook掉；选择什么地方Hook是一个很自由的事情，但是Hook的步骤越后越可靠——Hook得越早，后面的调用就越复杂，越容易出错。<br>我们可以选择在进入AMS进程的入口进行Hook，具体来说也就是Hook AMS在本进程的代理对象ActivityManagerNative。如果你不知道如何Hook掉这个AMS的代理对象，请查阅我之前的文章 Hook机制之AMS&amp;PMS<br>我们Hook掉ActivityManagerNative对于startActivity方法的调用，替换掉交给AMS的intent对象，将里面的TargetActivity的暂时替换成已经声明好的替身StubActivity；这种Hook方式 前文 讲述的很详细，不赘述；替换的关键代码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="string">"startActivity"</span>.equals(method.getName())) &#123;</span><br><span class="line"><span class="comment">// 只拦截这个方法</span></span><br><span class="line"><span class="comment">// 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱</span></span><br><span class="line"><span class="comment">// API 23:</span></span><br><span class="line"><span class="comment">// public final Activity startActivityNow(Activity parent, String id,</span></span><br><span class="line"><span class="comment">// Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,</span></span><br><span class="line"><span class="comment">// Activity.NonConfigurationInstances lastNonConfigurationInstances) &#123;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 找到参数里面的第一个Intent 对象</span></span><br><span class="line"></span><br><span class="line">Intent raw;</span><br><span class="line"><span class="keyword">int</span> index = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; args.length; i++) &#123;</span><br><span class="line"><span class="keyword">if</span> (args[i] <span class="keyword">instanceof</span> Intent) &#123;</span><br><span class="line">index = i;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">raw = (Intent) args[index];</span><br><span class="line"></span><br><span class="line">Intent newIntent = <span class="keyword">new</span> Intent();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里包名直接写死,如果再插件里,不同的插件有不同的包 传递插件的包名即可</span></span><br><span class="line">String targetPackage = <span class="string">"com.weishu.intercept_activity.app"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里我们把启动的Activity临时替换为 StubActivity</span></span><br><span class="line">ComponentName componentName = <span class="keyword">new</span> ComponentName(targetPackage, StubActivity.class.getCanonicalName());</span><br><span class="line">newIntent.setComponent(componentName);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把我们原始要启动的TargetActivity先存起来</span></span><br><span class="line">newIntent.putExtra(HookHelper.EXTRA_TARGET_INTENT, raw);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 替换掉Intent, 达到欺骗AMS的目的</span></span><br><span class="line">args[index] = newIntent;</span><br><span class="line"></span><br><span class="line">Log.d(TAG, <span class="string">"hook success"</span>);</span><br><span class="line"><span class="keyword">return</span> method.invoke(mBase, args);</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> method.invoke(mBase, args);</span><br></pre></td></tr></table></figure>

<p>通过这个替换过程，在ActivityManagerNative的startActivity调用之后，system_server端收到Binder驱动的消息，开始执行ActivityManagerService里面真正的startActivity方法；</p>
<p>startActivity(new Intent(MainActivity.this, TargetActivity.class));</p>
<p>也不会出现上文的ActivityNotFoundException</p>
<h5 id="借尸还魂——拦截Callback从恢复真身"><a href="#借尸还魂——拦截Callback从恢复真身" class="headerlink" title="借尸还魂——拦截Callback从恢复真身"></a>借尸还魂——拦截Callback从恢复真身</h5><p>行百里者半九十。现在我们的startActivity启动一个没有显式声明的Activity已经不会抛异常了，但是要真正正确地把TargetActivity启动起来，还有一些事情要做。其中最重要的一点是，我们用替身StubActivity临时换了TargetActivity，肯定需要在『合适的』时候替换回来；接下来我们就完成这个过程。<br>在AMS进程里面我们是没有办法换回来的，因此我们要等AMS把控制权交给App所在进程，也就是上面那个『Activity启动过程简图』的第三步。AMS进程转移到App进程也是通过Binder调用完成的，承载这个功能的Binder对象是IApplicationThread；在App进程它是Server端，在Server端接受Binder远程调用的是Binder线程池，Binder线程池通过Handler将消息转发给App的主线程；（我这里不厌其烦地叙述Binder调用过程，希望读者不要反感，其一加深印象，其二懂Binder真的很重要）我们可以在这个Handler里面将替身恢复成真身。<br>这里不打算讲述Handler 的原理，我们简单看一下Handler是如何处理接收到的Message的，如果我们能拦截这个Message的接收过程，就有可能完成替身恢复工作；Handler类的dispathMesage如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">dispatchMessage</span><span class="params">(Message msg)</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (msg.callback != <span class="keyword">null</span>) &#123;</span><br><span class="line">handleCallback(msg);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (mCallback != <span class="keyword">null</span>) &#123;</span><br><span class="line"><span class="keyword">if</span> (mCallback.handleMessage(msg)) &#123;</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">handleMessage(msg);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>从这个方法可以看出来，Handler类消息分发的过程如下：</p>
<p>如果传递的Message本身就有callback，那么直接使用Message对象的callback方法；<br>如果Handler类的成员变量mCallback存在，那么首先执行这个mCallback回调；<br>如果mCallback的回调返回true，那么表示消息已经成功处理；直接结束。<br>如果mCallback的回调返回false，那么表示消息没有处理完毕，会继续使用Handler类的handleMessage方法处理消息。<br>那么，ActivityThread中的Handler类H是如何实现的呢？H的部分源码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handleMessage</span><span class="params">(Message msg)</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (DEBUG_MESSAGES) Slog.v(TAG, <span class="string">"&gt;&gt;&gt; handling: "</span> + codeToString(msg.what));</span><br><span class="line"><span class="keyword">switch</span> (msg.what) &#123;</span><br><span class="line"><span class="keyword">case</span> LAUNCH_ACTIVITY: &#123;</span><br><span class="line">Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, <span class="string">"activityStart"</span>);</span><br><span class="line">ActivityClientRecord r = (ActivityClientRecord)msg.obj;</span><br><span class="line"></span><br><span class="line">r.packageInfo = getPackageInfoNoCheck(</span><br><span class="line">r.activityInfo.applicationInfo, r.compatInfo);</span><br><span class="line">handleLaunchActivity(r, <span class="keyword">null</span>);</span><br><span class="line">Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);</span><br><span class="line">&#125; <span class="keyword">break</span>;</span><br><span class="line"><span class="keyword">case</span> RELAUNCH_ACTIVITY: &#123;</span><br><span class="line">Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, <span class="string">"activityRestart"</span>);</span><br><span class="line">ActivityClientRecord r = (ActivityClientRecord)msg.obj;</span><br><span class="line">handleRelaunchActivity(r);</span><br><span class="line">Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以下略</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>可以看到<code>H</code>类仅仅重载了<code>handleMessage</code>方法；通过dispathMessage的消息分发过程得知，我们可以拦截这一过程：把这个<code>H</code>类的<code>mCallback</code>替换为我们的自定义实现，这样dispathMessage就会首先使用这个自定义的mCallback，然后看情况使用H重载的handleMessage。<br>这个Handler.Callback是一个接口，我们可以使用动态代理或者普通代理完成Hook，这里我们使用普通的静态代理方式；创建一个自定义的Callback类：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* package */</span> <span class="class"><span class="keyword">class</span> <span class="title">ActivityThreadHandlerCallback</span> <span class="keyword">implements</span> <span class="title">Handler</span>.<span class="title">Callback</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">Handler mBase;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ActivityThreadHandlerCallback</span><span class="params">(Handler base)</span> </span>&#123;</span><br><span class="line">mBase = base;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">handleMessage</span><span class="params">(Message msg)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">switch</span> (msg.what) &#123;</span><br><span class="line"><span class="comment">// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100</span></span><br><span class="line"><span class="comment">// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码</span></span><br><span class="line"><span class="keyword">case</span> <span class="number">100</span>:</span><br><span class="line">handleLaunchActivity(msg);</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">mBase.handleMessage(msg);</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">handleLaunchActivity</span><span class="params">(Message msg)</span> </span>&#123;</span><br><span class="line"><span class="comment">// 这里简单起见,直接取出TargetActivity;</span></span><br><span class="line"></span><br><span class="line">Object obj = msg.obj;</span><br><span class="line"><span class="comment">// 根据源码:</span></span><br><span class="line"><span class="comment">// 这个对象是 ActivityClientRecord 类型</span></span><br><span class="line"><span class="comment">// 我们修改它的intent字段为我们原来保存的即可.</span></span><br><span class="line"><span class="comment">/* switch (msg.what) &#123;</span></span><br><span class="line"><span class="comment">/ case LAUNCH_ACTIVITY: &#123;</span></span><br><span class="line"><span class="comment">/ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");</span></span><br><span class="line"><span class="comment">/ final ActivityClientRecord r = (ActivityClientRecord) msg.obj;</span></span><br><span class="line"><span class="comment">/</span></span><br><span class="line"><span class="comment">/ r.packageInfo = getPackageInfoNoCheck(</span></span><br><span class="line"><span class="comment">/ r.activityInfo.applicationInfo, r.compatInfo);</span></span><br><span class="line"><span class="comment">/ handleLaunchActivity(r, null);</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line"><span class="comment">// 把替身恢复成真身</span></span><br><span class="line">Field intent = obj.getClass().getDeclaredField(<span class="string">"intent"</span>);</span><br><span class="line">intent.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">Intent raw = (Intent) intent.get(obj);</span><br><span class="line"></span><br><span class="line">Intent target = raw.getParcelableExtra(HookHelper.EXTRA_TARGET_INTENT);</span><br><span class="line">raw.setComponent(target.getComponent());</span><br><span class="line"></span><br><span class="line">&#125; <span class="keyword">catch</span> (NoSuchFieldException e) &#123;</span><br><span class="line">e.printStackTrace();</span><br><span class="line">&#125; <span class="keyword">catch</span> (IllegalAccessException e) &#123;</span><br><span class="line">e.printStackTrace();</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这个Callback类的使命很简单：把替身StubActivity恢复成真身TargetActivity；有了这个自定义的Callback之后我们需要把ActivityThread里面处理消息的Handler类H的的mCallback修改为自定义callback类的对象：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 先获取到当前的ActivityThread对象</span></span><br><span class="line">Class&lt;?&gt; activityThreadClass = Class.forName(<span class="string">"android.app.ActivityThread"</span>);</span><br><span class="line">Field currentActivityThreadField = activityThreadClass.getDeclaredField(<span class="string">"sCurrentActivityThread"</span>);</span><br><span class="line">currentActivityThreadField.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">Object currentActivityThread = currentActivityThreadField.get(<span class="keyword">null</span>);<span class="comment">// 由于ActivityThread一个进程只有一个,我们获取这个对象的</span></span><br><span class="line">mHField mHField = activityThreadClass.getDeclaredField(<span class="string">"mH"</span>);</span><br><span class="line">mHField.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">Handler mH = (Handler) mHField.get(currentActivityThread);<span class="comment">// 设置它的回调, 根据源码:// 我们自己给他设置一个回调,就会</span></span><br><span class="line"> Handler.class.getDeclaredField(<span class="string">"mCallback"</span>);mCallBackField.setAccessible(<span class="keyword">true</span>);mCallBackField.set(mH, <span class="keyword">new</span> ActivityThreadHandlerCallback(mH));</span><br></pre></td></tr></table></figure>

<p>到这里，我们已经成功地绕过<code>AMS</code>，完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的过程；瞒天过海，这种玩弄系统与股掌之中的快感你们能体会到吗？</p>
<h5 id="僵尸or活人？——能正确收到生命周期回调吗"><a href="#僵尸or活人？——能正确收到生命周期回调吗" class="headerlink" title="僵尸or活人？——能正确收到生命周期回调吗"></a>僵尸or活人？——能正确收到生命周期回调吗</h5><p>虽然我们完成了『启动没有在AndroidManifest.xml中显式声明的Activity 』，但是启动的TargetActivity是否有自己的生命周期呢，我们还需要额外的处理过程吗？<br>实际上TargetActivity已经是一个有血有肉的Activity了：它具有自己正常的生命周期；可以运行Demo代码验证一下。<br>这个过程是如何完成的呢？我们以onDestroy为例简要分析一下：<br>从Activity的finish方法开始跟踪，最终会通过ActivityManagerNative到AMS然后接着通过ApplicationThread到ActivityThread，然后通过H转发消息到ActivityThread的handleDestroyActivity，接着这个方法把任务交给performDestroyActivity完成。<br>在真正分析这个方法之前，需要说明一点的是：不知读者是否感受得到，App进程与AMS交互几乎都是这么一种模式，几个角色 ActivityManagerNative, ApplicationThread, ActivityThread以及Handler类H分工明确，读者可以按照这几个角色的功能分析AMS的任何调用过程，屡试不爽；这也是我的初衷——希望分析插件框架的过程中能帮助深入理解Android Framework。<br>好了继续分析performDestroyActivity，关键代码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ActivityClientRecord r = mActivities.get(token);</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...略</span></span><br><span class="line"></span><br><span class="line">mInstrumentation.callActivityOnDestroy(r.activ</span><br></pre></td></tr></table></figure>

<p>这里通过mActivities拿到了一个ActivityClientRecord，然后直接把这个record里面的Activity交给Instrument类完成了onDestroy的调用。<br>在我们这个demo的场景下，r.activity是TargetActivity还是StubActivity？按理说，由于我们欺骗了AMS，AMS应该只知道StubActivity的存在，它压根儿就不知道TargetActivity是什么，为什么它能正确完成对TargetActivity生命周期的回调呢？<br>一切的秘密在token里面。AMS与ActivityThread之间对于Activity的生命周期的交互，并没有直接使用Activity对象进行交互，而是使用一个token来标识，这个token是binder对象，因此可以方便地跨进程传递。Activity里面有一个成员变量mToken代表的就是它，token可以唯一地标识一个Activity对象，它在Activity的attach方法里面初始化；<br>在AMS处理Activity的任务栈的时候，使用这个token标记Activity，因此在我们的demo里面，AMS进程里面的token对应的是StubActivity，也就是AMS还在傻乎乎地操作StubActivity（关于这一点，你可以dump出任务栈的信息，可以观察到dump出的确实是StubActivity）。但是在我们App进程里面，token对应的却是TargetActivity！因此，在ActivityThread执行回调的时候，能正确地回调到TargetActivity相应的方法。<br>为什么App进程里面，token对应的是TargetActivity呢？<br>回到代码，ActivityClientRecord是在mActivities里面取出来的，确实是根据token取；那么这个token是什么时候添加进去的呢？我们看performLaunchActivity就完成明白了：它通过classloader加载了TargetActivity，然后完成一切操作之后把这个activity添加进了mActivities！另外，在这个方法里面我们还能看到对Ativityattach方法的调用，它传递给了新创建的Activity一个token对象，而这个token是在ActivityClientRecord构造函数里面初始化的。<br>至此我们已经可以确认，通过这种方式启动的Activity有它自己完整而独立的生命周期！</p>
<h2 id="小节"><a href="#小节" class="headerlink" title="小节"></a>小节</h2><p>本文讲述了『启动一个并没有在AndroidManifest.xml中显示声明的Activity』的解决办法，我们成功地绕过了Android的这个限制，这个是插件Activity管理技术的基础；但是要做到启动一个插件Activity问题远没有这么简单。<br>首先，在Android中，Activity有不同的启动模式；我们声明了一个替身StubActivity，肯定没有满足所有的要求；因此，我们需要在AndroidManifest.xml中声明一系列的有不同launchMode的Activity，还需要完成替身与真正Activity launchMode的匹配过程；这样才能完成启动各种类型Activity的需求，关于这一点，在 DroidPlugin 的com.morgoo.droidplugin.stub包下面可以找到。<br>另外，每启动一个插件的Activity都需要一个StubActivity，但是AndroidManifest.xml中肯定只能声明有限个，如果一直startActivity而不finish的话，那么理论上就需要无限个StubActivity；这个问题该如何解决呢？事实上，这个问题在技术上没有好的解决办法。但是，如果你的App startActivity了十几次，而没有finish任何一个Activity，这样在Activity的回退栈里面有十几个Activity，用户难道按back十几次回到主页吗？有这种需求说明你的产品设计有问题；一个App一级页面，二级页面..到五六级的页面已经影响体验了，所以，每种LauchMode声明十个StubActivity绝对能满足需求了。<br>最后，在本文所述例子中，TargetActivity与StubActivity存在于同一个Apk，因此系统的ClassLoader能够成功加载并创建TargetActivity的实例。但是在实际的插件系统中，要启动的目标Activity肯定存在于一个单独的文件中，系统默认的ClassLoader无法加载插件中的Activity类——系统压根儿就不知道要加载的插件在哪，谈何加载？因此还有一个很重要的问题需要处理：<br>我们要完成插件系统中类的加载，这可以通过自定义ClassLoader实现。解决了『启动没有在AndroidManifest.xml中显式声明的，并且存在于外部文件中的Activity』的问题，插件系统对于Activity的管理才算得上是一个完全体。篇幅所限，欲知后事如何，请听下回分解！</p>

        </div>
        
          


  <section class='meta' id="footer-meta">
    <hr>
    <div class='new-meta-box'>
      
        
          <div class="new-meta-item date" itemprop="dateUpdated" datetime="2019-08-26T09:51:18+08:00">
  <a class='notlink'>
    <i class="fas fa-clock" aria-hidden="true"></i>
    <p>最后更新于 2019年8月26日</p>
  </a>
</div>

        
      
        
          
  
  <div class="new-meta-item meta-tags"><a class="tag" href="/tags/android快速开发/" rel="nofollow"><i class="fas fa-hashtag" aria-hidden="true"></i>&nbsp;<p>android快速开发</p></a></div> <div class="new-meta-item meta-tags"><a class="tag" href="/tags/hook/" rel="nofollow"><i class="fas fa-hashtag" aria-hidden="true"></i>&nbsp;<p>hook</p></a></div>


        
      
        
          
  <div class="new-meta-item share -mob-share-list">
  <div class="-mob-share-list share-body">
    
      
        <a class="-mob-share-qq" title="QQ好友" rel="external nofollow noopener noreferrer"
          
          href="http://connect.qq.com/widget/shareqq/index.html?url=https://zyqing.ml/2018/02/19/data/hookAndroid2/&title=Android 插件化原理解析——Activity生命周期管理 | 会飞的猪&summary=Activity生命周期管理揭开了Hook机制的神秘面纱，现在我们手握倚天屠龙，那么如何通过这种技术完成插件化方案呢？具体来说，插件中的Activity，Service等组件如何在Android系统上运行起来？"
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/xaoxuu/assets@19.1.9/logo/128/qq.png">
          
        </a>
      
    
      
        <a class="-mob-share-qzone" title="QQ空间" rel="external nofollow noopener noreferrer"
          
          href="https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=https://zyqing.ml/2018/02/19/data/hookAndroid2/&title=Android 插件化原理解析——Activity生命周期管理 | 会飞的猪&summary=Activity生命周期管理揭开了Hook机制的神秘面纱，现在我们手握倚天屠龙，那么如何通过这种技术完成插件化方案呢？具体来说，插件中的Activity，Service等组件如何在Android系统上运行起来？"
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/xaoxuu/assets@19.1.9/logo/128/qzone.png">
          
        </a>
      
    
      
        <a class="-mob-share-weibo" title="微博" rel="external nofollow noopener noreferrer"
          
          href="http://service.weibo.com/share/share.php?url=https://zyqing.ml/2018/02/19/data/hookAndroid2/&title=Android 插件化原理解析——Activity生命周期管理 | 会飞的猪&summary=Activity生命周期管理揭开了Hook机制的神秘面纱，现在我们手握倚天屠龙，那么如何通过这种技术完成插件化方案呢？具体来说，插件中的Activity，Service等组件如何在Android系统上运行起来？"
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/xaoxuu/assets@19.1.9/logo/128/weibo.png">
          
        </a>
      
    
  </div>
</div>



        
      
    </div>
  </section>


        
        
            <div class="prev-next">
                
                    <section class="prev">
                        <span class="art-item-left">
                            <h6><i class="fas fa-chevron-left" aria-hidden="true"></i>&nbsp;上一页</h6>
                            <h4>
                                <a href="/2018/06/09/data/rxjava1/" rel="prev" title="rxjava">
                                  
                                      rxjava
                                  
                                </a>
                            </h4>
                            
                                
                                <h6 class="tags">
                                    <a class="tag" href="/tags/android快速开发/"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i>android快速开发</a> <a class="tag" href="/tags/rxjava/"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i>rxjava</a>
                                </h6>
                            
                        </span>
                    </section>
                
                
                    <section class="next">
                        <span class="art-item-right" aria-hidden="true">
                            <h6>下一页&nbsp;<i class="fas fa-chevron-right" aria-hidden="true"></i></h6>
                            <h4>
                                <a href="/2018/01/06/data/hookAndroid1/" rel="prev" title="Android插件化原理解析——Hook机制之动态代理">
                                    
                                        Android插件化原理解析——Hook机制之动态代理
                                    
                                </a>
                            </h4>
                            
                                
                                <h6 class="tags">
                                    <a class="tag" href="/tags/android快速开发/"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i>android快速开发</a> <a class="tag" href="/tags/hook/"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i>hook</a>
                                </h6>
                            
                        </span>
                    </section>
                
            </div>
        
      </section>
    </article>
  

  
    <!-- 显示推荐文章和评论 -->



  <article class="post white-box comments">
    <section class="article typo">
      <h4><i class="fas fa-comments fa-fw" aria-hidden="true"></i>&nbsp;评论</h4>
      
      
      
      
        <section id="comments">
          <div id="valine_container" class="valine_thread">
            <i class="fas fa-spinner fa-spin fa-fw"></i>
          </div>
        </section>
      
    </section>
  </article>


  




<!-- 根据页面mathjax变量决定是否加载MathJax数学公式js -->



  <script>
    window.subData = {
      title: 'Android 插件化原理解析——Activity生命周期管理',
      tools: true
    }
  </script>


</div>
<aside class='l_side'>
  
    
    
      
        
          
          
            <section class='widget author'>
  <div class='content material'>
    
      <div class='avatar'>
        <img class='avatar' src='http://zy_qing.gitee.io/resources/img/touxiang.jpg'/>
      </div>
    
     
      <div class='text'>
        
        
        
          <p><span id="jinrishici-sentence">柳暗花明又一村</span></p>
          <script src="https://sdk.jinrishici.com/v2/browser/jinrishici.js" charset="utf-8"></script>
        
      </div>
    
    
      <div class="social-wrapper">
        
          
            <a href="mailto:1091425078@qq.com"
              class="social fas fa-envelope flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
            </a>
          
        
          
            <a href="https://github.com/YongQingAndroid"
              class="social fab fa-github flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
            </a>
          
        
          
            <a href="https://music.163.com/#/user/home?id=343100855"
              class="social fas fa-headphones-alt flat-btn"
              target="_blank"
              rel="external nofollow noopener noreferrer">
            </a>
          
        
      </div>
    
  </div>
</section>

          
        
      
        
          
          
            <section class='widget list'>
  
<header class='material'>
  <div><i class="fas fa-map-signs fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;常用功能</div>
  
</header>

  <div class='content material'>
    <ul class="entry">
      
        <li><a class="flat-box" title="https://dev.tencent.com/u/zyqandroid/p/blog/git" href="https://dev.tencent.com/u/zyqandroid/p/blog/git"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-clock fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;我的GIT
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/markPage/" href="/markPage/"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-archive fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;本地markDown工具
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/2md/" href="/2md/"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-book fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;HTML转MD
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="http://zy_qing.gitee.io/resources/" href="http://zy_qing.gitee.io/resources/"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-archive fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;备用markDown工具
          </div>
          
        </a></li>
      
    </ul>
  </div>
</section>

          
        
      
        
          
          
            
  <section class='widget toc-wrapper'>
    
<header class='material'>
  <div><i class="fas fa-list fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;本文目录</div>
  
    <div class='wrapper'><a class="s-toc rightBtn" rel="external nofollow noopener noreferrer" href="javascript:void(0)"><i class="fas fa-thumbtack fa-fw"></i></a></div>
  
</header>

    <div class='content material'>
      <ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#Activity生命周期管理"><span class="toc-text">Activity生命周期管理</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#AndroidManifest-xml的限制"><span class="toc-text">AndroidManifest.xml的限制</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Activity启动过程"><span class="toc-text">Activity启动过程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#瞒天过海——启动不在AndroidManifest-xml中声明的Activity"><span class="toc-text">瞒天过海——启动不在AndroidManifest.xml中声明的Activity</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#简要分析"><span class="toc-text">简要分析</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#实战过程"><span class="toc-text">实战过程</span></a><ol class="toc-child"><li class="toc-item toc-level-5"><a class="toc-link" href="#狸猫换太子——使用替身Activity绕过AMS"><span class="toc-text">狸猫换太子——使用替身Activity绕过AMS</span></a></li><li class="toc-item toc-level-5"><a class="toc-link" href="#借尸还魂——拦截Callback从恢复真身"><span class="toc-text">借尸还魂——拦截Callback从恢复真身</span></a></li><li class="toc-item toc-level-5"><a class="toc-link" href="#僵尸or活人？——能正确收到生命周期回调吗"><span class="toc-text">僵尸or活人？——能正确收到生命周期回调吗</span></a></li></ol></li></ol></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#小节"><span class="toc-text">小节</span></a></li></ol></li></ol>
    </div>
  </section>


          
        
      
        
          
          
            <section class='widget list'>
  
<header class='material'>
  <div><i class="fas fa-map-signs fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;站内导航</div>
  
</header>

  <div class='content material'>
    <ul class="entry">
      
        <li><a class="flat-box" title="/" href="/"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-clock fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;近期文章
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/blog/archives/" href="/blog/archives/"
          
            rel="nofollow"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-archive fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;文章归档
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/projects/" href="/projects/"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-code-branch fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;开源项目
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/friends/" href="/friends/"
          
            rel="nofollow"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-link fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;我的友链
          </div>
          
        </a></li>
      
        <li><a class="flat-box" title="/about/" href="/about/"
          
            rel="nofollow"
          
          
          >
          <div class='name'>
            
              <i class="fas fa-info-circle fa-fw" aria-hidden="true"></i>
            
            &nbsp;&nbsp;关于我
          </div>
          
        </a></li>
      
    </ul>
  </div>
</section>

          
        
      
        
          
          
            
  <section class='widget category'>
    
<header class='material'>
  <div><i class="fas fa-folder-open fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;文章分类</div>
  
    <a class="rightBtn"
    
      rel="nofollow"
    
    
    href="/blog/categories/"
    title="blog/categories/">
    <i class="fas fa-expand-arrows-alt fa-fw"></i></a>
  
</header>

    <div class='content material'>
      <ul class="entry">
        
          <li><a class="flat-box" title="/categories/Android/" href="/categories/Android/"><div class='name'>Android</div><div class='badge'>(6)</div></a></li>
        
          <li><a class="flat-box" title="/categories/教程/" href="/categories/教程/"><div class='name'>教程</div><div class='badge'>(1)</div></a></li>
        
      </ul>
    </div>
  </section>


          
        
      
        
          
          
            
  <section class='widget tagcloud'>
    
<header class='material'>
  <div><i class="fas fa-fire fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;热门标签</div>
  
    <a class="rightBtn"
    
      rel="nofollow"
    
    
    href="/blog/tags/"
    title="blog/tags/">
    <i class="fas fa-expand-arrows-alt fa-fw"></i></a>
  
</header>

    <div class='content material'>
      <a href="/tags/android/" style="font-size: 14px; color: #999">android</a> <a href="/tags/android快速开发/" style="font-size: 24px; color: #555">android快速开发</a> <a href="/tags/hook/" style="font-size: 19px; color: #777">hook</a> <a href="/tags/rxjava/" style="font-size: 14px; color: #999">rxjava</a> <a href="/tags/测试标签/" style="font-size: 19px; color: #777">测试标签</a>
    </div>
  </section>


          
        
      
        
          
          
            


  <section class='widget music'>
    
<header class='material'>
  <div><i class="fas fa-compact-disc fa-fw" aria-hidden="true"></i>&nbsp;&nbsp;最近在听</div>
  
    <a class="rightBtn"
    
      rel="external nofollow noopener noreferrer"
    
    
      target="_blank"
    
    href="https://music.163.com/#/user/home?id=343100855"
    title="https://music.163.com/#/user/home?id=343100855">
    <i class="far fa-heart fa-fw"></i></a>
  
</header>

    <div class='content material'>
      
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer@1.7.0/dist/APlayer.min.css">
  <div class="aplayer"
    data-theme="#1BCDFC"
    
    
    data-mode="circulation"
    data-server="netease"
    data-type="playlist"
    data-id="481100619"
    data-volume="0.7">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/aplayer@1.7.0/dist/APlayer.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/meting@1.1.0/dist/Meting.min.js"></script>


    </div>
  </section>


          
        
      
    

  
</aside>

<footer id="footer" class="clearfix">
  
  
    <div class="social-wrapper">
      
        
          <a href="mailto:1091425078@qq.com"
            class="social fas fa-envelope flat-btn"
            target="_blank"
            rel="external nofollow noopener noreferrer">
          </a>
        
      
        
          <a href="https://github.com/YongQingAndroid"
            class="social fab fa-github flat-btn"
            target="_blank"
            rel="external nofollow noopener noreferrer">
          </a>
        
      
        
          <a href="https://music.163.com/#/user/home?id=343100855"
            class="social fas fa-headphones-alt flat-btn"
            target="_blank"
            rel="external nofollow noopener noreferrer">
          </a>
        
      
    </div>
  
  <br>
  <div><p>博客内容遵循 <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh">署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议</a></p>
</div>
  <div>
    本站使用
    <a href="https://xaoxuu.com/wiki/material-x/" target="_blank" class="codename">Material X</a>
    作为主题
    
      ，
      总访问量为
      <span id="busuanzi_value_site_pv"><i class="fas fa-spinner fa-spin fa-fw" aria-hidden="true"></i></span>
      次
    
    。
  </div>
  <!-- 血小板 -->
  <div id="landlord">
      <div class="message" style="opacity:0"></div>
      <canvas id="live2d" width="560" height="500" class="live2d"></canvas>
      <div class="hide-button">隐藏</div>
  </div>
</footer>
<script>setLoadingBarProgress(80);</script>
<!-- live2d 血小板 -->
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
    var message_Path = '/live2d/'
    var home_Path = 'https://haremu.com/'
</script>
<script type="text/javascript" src="/live2d/js/live2d.js"></script>
<script type="text/javascript" src="/live2d/js/message.js"></script>
<script type="text/javascript">
    loadlive2d("live2d", "/live2d/model/xiaoban/model.json");
</script>


      <script>setLoadingBarProgress(60);</script>
    </div>
    <a class="s-top fas fa-arrow-up fa-fw" href='javascript:void(0)'></a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>

  <script>
    var GOOGLE_CUSTOM_SEARCH_API_KEY = "";
    var GOOGLE_CUSTOM_SEARCH_ENGINE_ID = "";
    var ALGOLIA_API_KEY = "";
    var ALGOLIA_APP_ID = "";
    var ALGOLIA_INDEX_NAME = "";
    var AZURE_SERVICE_NAME = "";
    var AZURE_INDEX_NAME = "";
    var AZURE_QUERY_KEY = "";
    var BAIDU_API_ID = "";
    var SEARCH_SERVICE = "hexo" || "hexo";
    var ROOT = "/"||"/";
    if(!ROOT.endsWith('/'))ROOT += '/';
  </script>

<script src="//instant.page/1.2.2" type="module" integrity="sha384-2xV8M5griQmzyiY3CDqh1dn4z3llDVqZDqzjzcY+jCBCk/a5fXJmuZ/40JJAPeoU"></script>


  <script async src="https://cdn.jsdelivr.net/npm/scrollreveal@4.0.5/dist/scrollreveal.min.js"></script>
  <script type="text/javascript">
    $(function() {
      const $reveal = $('.reveal');
      if ($reveal.length === 0) return;
      const sr = ScrollReveal({ distance: 0 });
      sr.reveal('.reveal');
    });
  </script>


  <script src="https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.js"></script>
  <script type="text/javascript">
    $(function() {
      Waves.attach('.flat-btn', ['waves-button']);
      Waves.attach('.float-btn', ['waves-button', 'waves-float']);
      Waves.attach('.float-btn-light', ['waves-button', 'waves-float', 'waves-light']);
      Waves.attach('.flat-box', ['waves-block']);
      Waves.attach('.float-box', ['waves-block', 'waves-float']);
      Waves.attach('.waves-image');
      Waves.init();
    });
  </script>


  <script async src="https://cdn.jsdelivr.net/gh/xaoxuu/cdn-busuanzi@2.3/js/busuanzi.pure.mini.js"></script>












  <script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
  
    
      <script src="https://cdn.jsdelivr.net/gh/xaoxuu/volantis@1.0/js/volantis.min.js"></script>
    
  
  <script>
  var GUEST_INFO = ['nick','mail','link'];
  var guest_info = 'nick,mail,link'.split(',').filter(function(item){
    return GUEST_INFO.indexOf(item) > -1
  });
  var notify = 'true' == true;
  var verify = 'true' == true;
  var valine = new Valine();
  valine.init({
    el: '#valine_container',
    notify: notify,
    verify: verify,
    guest_info: guest_info,
    
    appId: "nbvwRDSIj9302hYN0p3EgCgT-gzGzoHsz",
    appKey: "XVOUM8qKVsqegg2TnN7sHpMQ",
    placeholder: "快来评论吧~",
    pageSize:'10',
    avatar:'mp',
    lang:'zh-cn',
    highlight:'true'
  })
  </script>



  <script src="https://cdn.jsdelivr.net/gh/xaoxuu/cdn-material-x@19.5/js/app.js"></script>


  <script src="https://cdn.jsdelivr.net/gh/xaoxuu/cdn-material-x@19.5/js/search.js"></script>




<!-- 复制 -->
<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  let COPY_SUCCESS = "复制成功";
  let COPY_FAILURE = "复制失败";
  /*页面载入完成后，创建复制按钮*/
  !function (e, t, a) {
    /* code */
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '  <i class="fa fa-copy"></i><span>复制</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });

      clipboard.on('success', function(e) {
        //您可以加入成功提示
        console.info('Action:', e.action);
        console.info('Text:', e.text);
        console.info('Trigger:', e.trigger);
        success_prompt(COPY_SUCCESS);
        e.clearSelection();
      });
      clipboard.on('error', function(e) {
        //您可以加入失败提示
        console.error('Action:', e.action);
        console.error('Trigger:', e.trigger);
        fail_prompt(COPY_FAILURE);
      });
    }
    initCopyCode();

  }(window, document);

  /**
   * 弹出式提示框，默认1.5秒自动消失
   * @param message 提示信息
   * @param style 提示样式，有alert-success、alert-danger、alert-warning、alert-info
   * @param time 消失时间
   */
  var prompt = function (message, style, time)
  {
      style = (style === undefined) ? 'alert-success' : style;
      time = (time === undefined) ? 1500 : time*1000;
      $('<div>')
          .appendTo('body')
          .addClass('alert ' + style)
          .html(message)
          .show()
          .delay(time)
          .fadeOut();
  };

  // 成功提示
  var success_prompt = function(message, time)
  {
      prompt(message, 'alert-success', time);
  };

  // 失败提示
  var fail_prompt = function(message, time)
  {
      prompt(message, 'alert-danger', time);
  };

  // 提醒
  var warning_prompt = function(message, time)
  {
      prompt(message, 'alert-warning', time);
  };

  // 信息提示
  var info_prompt = function(message, time)
  {
      prompt(message, 'alert-info', time);
  };

</script>


<!-- fancybox -->
<script src="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js"></script>
<script>
  let LAZY_LOAD_IMAGE = "";
  $(".article-entry").find("fancybox").find("img").each(function () {
      var element = document.createElement("a");
      $(element).attr("data-fancybox", "gallery");
      $(element).attr("href", $(this).attr("src"));
      /* 图片采用懒加载处理时,
       * 一般图片标签内会有个属性名来存放图片的真实地址，比如 data-original,
       * 那么此处将原本的属性名src替换为对应属性名data-original,
       * 修改如下
       */
       if (LAZY_LOAD_IMAGE) {
         $(element).attr("href", $(this).attr("data-original"));
       }
      $(this).wrap(element);
  });
</script>





  <script>setLoadingBarProgress(100);</script>

</body>
</html>
<!--单击显示文字-->
<script type="text/javascript" src="/js/click_show_text.js"></script>

